]> dgit.raspbian.org Git - ocaml-cohttp.git/commitdiff
New upstream version 6.3.0
authorStéphane Glondu <steph@glondu.net>
Fri, 18 Sep 2026 06:35:16 +0000 (08:35 +0200)
committerStéphane Glondu <steph@glondu.net>
Fri, 18 Sep 2026 06:35:16 +0000 (08:35 +0200)
69 files changed:
.ocamlformat
CHANGES.md
cohttp-async.opam
cohttp-async/bin/cohttp_curl_async.ml
cohttp-async/bin/cohttp_server_async.ml
cohttp-async/examples/s3_cp.ml
cohttp-async/src/client.ml
cohttp-async/test/cohttp_async_test/src/cohttp_async_test.ml
cohttp-async/test/test_async_integration.ml
cohttp-bench.opam
cohttp-curl-async.opam
cohttp-curl-async/src/cohttp_curl_async.ml
cohttp-curl-lwt.opam
cohttp-curl-lwt/src/cohttp_curl_lwt.ml
cohttp-curl.opam
cohttp-eio.opam
cohttp-eio/src/server.ml
cohttp-eio/tests/test_forward_proxy.ml
cohttp-lwt-jsoo.opam
cohttp-lwt-jsoo/src/cohttp_lwt_jsoo.ml
cohttp-lwt-jsoo/src/cohttp_lwt_jsoo.mli
cohttp-lwt-jsoo/test/src/cohttp_lwt_jsoo_test.ml
cohttp-lwt-unix-bin.opam
cohttp-lwt-unix.opam
cohttp-lwt-unix/bin/cohttp_server_lwt.ml
cohttp-lwt-unix/examples/client_lwt_proxy.ml
cohttp-lwt-unix/src/debug.mli
cohttp-lwt-unix/src/server.mli
cohttp-lwt-unix/test/cohttp_lwt_unix_test/src/cohttp_lwt_unix_test.ml
cohttp-lwt-unix/test/test_client.ml
cohttp-lwt-unix/test/test_sanity.ml
cohttp-lwt-unix/test/test_sanity_noisy.ml
cohttp-lwt.opam
cohttp-lwt/src/connection.ml
cohttp-lwt/src/connection_cache.ml
cohttp-lwt/src/s.ml
cohttp-lwt/src/server.ml
cohttp-mirage.opam
cohttp-mirage/examples/dune [new file with mode: 0644]
cohttp-mirage/examples/static_server.ml [new file with mode: 0644]
cohttp-mirage/src/client.ml
cohttp-mirage/src/dune
cohttp-mirage/src/io.ml
cohttp-mirage/src/io.mli
cohttp-mirage/src/static.ml
cohttp-mirage/src/static.mli
cohttp-mirage/test/dune [new file with mode: 0644]
cohttp-mirage/test/test_static.ml [new file with mode: 0644]
cohttp-server-lwt-unix.opam
cohttp-server-lwt-unix/src/cohttp_server_lwt_unix.ml
cohttp-server-lwt-unix/src/cohttp_server_lwt_unix.mli
cohttp-top.opam
cohttp.opam
cohttp/src/cohttp.ml
cohttp/src/path.ml
cohttp/src/path.mli
cohttp/src/proxy.ml
cohttp/src/response.ml
cohttp/src/server.ml
cohttp/test/dune
cohttp/test/test_path.ml
cohttp/test/test_response.ml [new file with mode: 0644]
dune-project
flake.nix
http.opam
http/src/http.ml
http/src/http.mli
http/test/test_request.ml
http/test/test_response.ml

index 291f8a05cf17846bfba34af221499a1f18e41dfd..d6d27d91dd5ab8cbdec1f7b3bbae2adf8570c5ad 100644 (file)
@@ -1,4 +1,4 @@
-version=0.27.0
+version=0.29.0
 profile=conventional
 break-infix=fit-or-vertical
 parse-docstrings=true
index 33715027db42a53cfb582d3ca545d6a9d545c7a6..475822dca4a8409e3a743180ba78c4dab13f01c4 100644 (file)
@@ -1,3 +1,49 @@
+## v6.3.0 (2026-08-20)
+
+- cohttp: `Cohttp.Path.resolve_local_file` no longer escapes the docroot when
+  given percent-encoded traversal sequences such as `..%2f..%2f`. The URI path
+  is percent-decoded exactly once before `.` and `..` segments are removed.
+  (#1145 @avsm and Sapphire Livingstone, review by @mdales @edwintorok @patricoferris)
+
+- cohttp: `Cohttp.Path.resolve_local_file` collapses empty path segments and
+  drops a trailing slash. A request for `/dir//sub/` now resolves to
+  `docroot/dir/sub` instead of `docroot/dir//sub/`. (#1145 @avsm)
+
+- cohttp: Add `Cohttp.Path.normalise`, which converts a request URI into a
+  relative path that cannot ascend above its root. Servers that make access
+  control decisions on path segments must apply it to `Request.uri` before
+  inspecting them, as `Request.uri` does not normalise absolute-form or
+  percent-encoded targets. (#1145 @avsm)
+  ```ocaml
+  let callback _conn req _body =
+    let uri = Cohttp.Request.uri req in
+    match String.split_on_char '/' (Cohttp.Path.normalise uri) with
+    | "admin" :: _ when not (authorised req) -> Server.respond_not_found ()
+    | _ ->
+        let fname = Cohttp.Path.resolve_local_file ~docroot ~uri in
+        Server.respond_file ~fname ()
+  ```
+  Normalisation is not applied by default, as existing code may depend on the
+  present semantics, which are safe when not combined with local file
+  resolution.
+
+- cohttp-mirage: The static file server normalises the request path of every
+  request, including directory requests, before looking it up in the mirage-kv
+  store. Keys are now percent-decoded, so `/my%20file.txt` retrieves the key
+  `my file.txt` rather than `my%20file.txt` (#1145 @avsm, review by @mdales @edwintorok)
+
+- cohttp-mirage: The `request_fn` callback receives the request URI unchanged.
+  A request that falls back to an index page previously received a URI
+  rewritten to that page. (#1145 @avsm)
+
+- cohttp: do not add `Transfer-Encoding`/`Content-Length` framing headers to
+  responses that cannot have a body (1xx, 204 and 304). This fixes WebSocket
+  handshakes. (@mefyl @avsm, #1141)
+
+- http: add `Status.body_allowed`, the response-status counterpart to the
+  existing `Method.body_allowed`, for users constructing responses by hand
+  (#1141)
+
 ## v6.2.2 (2026-07-26)
 
 - cohttp: remove duplicate occurance of lwt in dune-project (@Alizter, #1138)
index 005209332b62f762c6a56db2aea4d8d3b9d1a91e..aa633d06cc0415aff23f75a2e5b325e2e5634cc3 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for the Async concurrency library"
@@ -26,7 +26,7 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "dune" {>= "3.8"}
-  "ocaml" {>= "4.14" & < "5.3.0"}
+  "ocaml" {>= "4.14"}
   "http" {= version}
   "cohttp" {= version}
   "async_kernel" {>= "v0.17.0"}
index b0903ab3559d1647033ad2571f7f0a1a0d6681c1..dfca2c627a337d0eb9ce815c6013dc8b0939305a 100644 (file)
@@ -32,8 +32,8 @@ let make_net_req uri meth' body () =
   body
   |> Body.to_pipe
   |> Pipe.iter ~f:(fun b ->
-         Stdlib.print_string b;
-         return ())
+      Stdlib.print_string b;
+      return ())
 
 let _ =
   (* enable logging to stdout *)
index 5ecaa22ddc24d8c165e13db238dc752a418a079d..84ee0d8f9e33c2fabec83fc7d50093747a51c451 100644 (file)
@@ -51,13 +51,13 @@ let serve ~info ~docroot ~index uri path =
                 (* Do a directory listing *)
                 Sys.ls_dir file_name
                 >>= Deferred.List.map ~how:`Parallel ~f:(fun f ->
-                        let file_name = file_name / f in
-                        try_with (fun () ->
-                            Unix.stat file_name >>| fun stat ->
-                            (Some stat.Unix.Stats.kind, stat.Unix.Stats.size, f))
-                        >>| function
-                        | Ok v -> v
-                        | Error _ -> (None, 0L, f))
+                    let file_name = file_name / f in
+                    try_with (fun () ->
+                        Unix.stat file_name >>| fun stat ->
+                        (Some stat.Unix.Stats.kind, stat.Unix.Stats.size, f))
+                    >>| function
+                    | Ok v -> v
+                    | Error _ -> (None, 0L, f))
                 >>= fun listing ->
                 html_of_listing uri path
                   (sort ((Some `Directory, 0L, "..") :: listing))
index 6c5cdbbd9ec8ba236c8f765e454346a0d7eee07e..1d396e06a4bbb031b8fdbbf44721fee4825ef193 100644 (file)
@@ -209,8 +209,7 @@ module Auth = struct
     let canonical_headers =
       sorted_headers
       |> List.fold ~init:"" ~f:(fun acc (k, v) ->
-             acc
-             ^ Printf.sprintf "%s:%s\n" (String.lowercase k) (String.strip v))
+          acc ^ Printf.sprintf "%s:%s\n" (String.lowercase k) (String.strip v))
     in
     let signed_headers =
       sorted_headers |> List.map ~f:(fun (k, _) -> k) |> String.concat ~sep:";"
index b8f805bf6d5cf269720b364fbf9621fc0ce00141..d52c5520a0d0b79419bbea3a352830bde89a8a76 100644 (file)
@@ -20,19 +20,21 @@ module Net = struct
 
   let connect_uri ?interrupt ?ssl_config uri =
     (match Uri.scheme uri with
-    | Some "httpunix" ->
-        let host = Uri.host_with_default ~default:"localhost" uri in
-        return @@ `Unix_domain_socket host
-    | _ -> (
-        lookup uri |> Deferred.Or_error.ok_exn >>= fun (host, addr, port) ->
-        return
-        @@
-        match (Uri.scheme uri, ssl_config) with
-        | Some "https", Some config -> `OpenSSL (addr, port, config)
-        | Some "https", None ->
-            let config = Conduit_async.V2.Ssl.Config.create ~hostname:host () in
-            `OpenSSL (addr, port, config)
-        | _ -> `TCP (addr, port)))
+      | Some "httpunix" ->
+          let host = Uri.host_with_default ~default:"localhost" uri in
+          return @@ `Unix_domain_socket host
+      | _ -> (
+          lookup uri |> Deferred.Or_error.ok_exn >>= fun (host, addr, port) ->
+          return
+          @@
+          match (Uri.scheme uri, ssl_config) with
+          | Some "https", Some config -> `OpenSSL (addr, port, config)
+          | Some "https", None ->
+              let config =
+                Conduit_async.V2.Ssl.Config.create ~hostname:host ()
+              in
+              `OpenSSL (addr, port, config)
+          | _ -> `TCP (addr, port)))
     >>= fun mode ->
     Conduit_async.V2.connect ?interrupt mode >>| fun (r, w) ->
     (Input_channel.create r, w)
@@ -134,22 +136,23 @@ let call ?interrupt ?ssl_config ?headers ?(chunked = false) ?(body = `Empty)
   (* Create a request, then make the request. Figure out an appropriate
      transfer encoding *)
   (match chunked with
-  | false ->
-      Body.Private.disable_chunked_encoding body >>| fun (body, body_length) ->
-      ( Cohttp.Request.make_for_client ?headers ~chunked ~body_length meth uri,
-        body )
-  | true ->
-      Deferred.return
-        (match Body.is_empty body with
-        | `True ->
-            (* Don't used chunked encoding with an empty body *)
-            ( Cohttp.Request.make_for_client ?headers ~chunked:false
-                ~body_length:0L meth uri,
-              body )
-        | `Unknown | `False ->
-            (* Use chunked encoding if there is a body *)
-            ( Cohttp.Request.make_for_client ?headers ~chunked:true meth uri,
-              body )))
+    | false ->
+        Body.Private.disable_chunked_encoding body
+        >>| fun (body, body_length) ->
+        ( Cohttp.Request.make_for_client ?headers ~chunked ~body_length meth uri,
+          body )
+    | true ->
+        Deferred.return
+          (match Body.is_empty body with
+          | `True ->
+              (* Don't used chunked encoding with an empty body *)
+              ( Cohttp.Request.make_for_client ?headers ~chunked:false
+                  ~body_length:0L meth uri,
+                body )
+          | `Unknown | `False ->
+              (* Use chunked encoding if there is a body *)
+              ( Cohttp.Request.make_for_client ?headers ~chunked:true meth uri,
+                body )))
   >>= fun (req, body) -> request ?interrupt ?ssl_config ~body ~uri req
 
 let get ?interrupt ?ssl_config ?headers uri =
index 7215983c2f7102074113b6113d827256df52263e..3673c2df58101e5ae0108c6820b5f3802167d6fa 100644 (file)
@@ -50,19 +50,17 @@ let test_server_s ?port ?(name = "Cohttp Server Test") spec f =
       let results =
         tests
         |> Deferred.List.map ~how:`Sequential ~f:(fun (name, test) ->
-               Logs.debug (fun m -> m "Running %s" name);
-               let res =
-                 try_with test >>| function
-                 | Ok () -> `Ok
-                 | Error exn -> `Exn exn
-               in
-               res >>| fun res -> (name, res))
+            Logs.debug (fun m -> m "Running %s" name);
+            let res =
+              try_with test >>| function Ok () -> `Ok | Error exn -> `Exn exn
+            in
+            res >>| fun res -> (name, res))
       in
       results >>| fun results ->
       let ounit_tests =
         results
         |> List.map ~f:(fun (name, res) ->
-               name >:: fun () -> match res with `Ok -> () | `Exn x -> raise x)
+            name >:: fun () -> match res with `Ok -> () | `Exn x -> raise x)
       in
       name >::: ounit_tests)
 
index a2cfb9a2bfe14486b62654dd942b806c1df79550..12363b9bcf56cff06486d6097f74edf42d50216d 100644 (file)
@@ -82,9 +82,9 @@ let ts =
         responses |> Pipe.to_list >>= fun resps ->
         resps
         |> Deferred.List.iter ~how:`Sequential ~f:(fun (_resp, body) ->
-               let expected_body = body_q |> Queue.dequeue_exn in
-               body |> Body.to_string >>| fun body ->
-               assert_equal ~printer expected_body body)
+            let expected_body = body_q |> Queue.dequeue_exn in
+            body |> Body.to_string >>| fun body ->
+            assert_equal ~printer expected_body body)
       in
       let large_chunked_response () =
         Client.get ~headers uri >>= fun (resp, body) ->
index 9e5bdf8d0dbe72c6d8af65aaed995ee0302c9da8..48c170cca84eebf4f3456f164f31a02e43e2b45d 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Benchmarks binaries for Cohttp"
index 5f8dc544c38dfceb49b7bc02139251ba3e3ae712..c05ebd7c1723f2c354d170a58fa8b85316c12a86 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Cohttp client using Curl & Async as the backend"
@@ -22,7 +22,7 @@ doc: "https://mirage.github.io/ocaml-cohttp/"
 bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "dune" {>= "3.8"}
-  "ocurl" {>= "0.9.2"}
+  "curl" {>= "0.10.0"}
   "http" {= version}
   "stringext"
   "cohttp-curl" {= version}
index ef41336f959fd3eac010d01f3b7e18a95411abae..dd48828b7af9c1468a9e8ee7fcae23f50c82bbd4 100644 (file)
@@ -74,9 +74,9 @@ module Context = struct
             (fun () -> f ())
             ()
           |> Deferred.map ~f:(function
-               | `Bad_fd | `Closed -> assert false
-               | `Unsupported -> assert false
-               | `Interrupted -> ())
+            | `Bad_fd | `Closed -> assert false
+            | `Unsupported -> assert false
+            | `Interrupted -> ())
           |> Deferred.ignore_m
         in
         don't_wait_for event;
index 735ad62e40f864c220505790eb8cf7f89d411783..605ab5b536f3dd9eb6a54322ce10c364024cfee3 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Cohttp client using Curl & Lwt as the backend"
@@ -23,7 +23,7 @@ bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "dune" {>= "3.8"}
   "ocaml" {>= "4.08"}
-  "ocurl" {>= "0.9.2"}
+  "curl" {>= "0.10.0"}
   "http" {= version}
   "cohttp-curl" {= version}
   "stringext"
index e856024487440bfd8d9f62aecfd91f4faef2b76d..e75a6adb8457cbfde1837315c47a45ef4f895c69 100644 (file)
@@ -40,11 +40,10 @@ module Context = struct
   let unregister t curl =
     Curl.get_activesocket curl
     |> Option.iter (fun fd ->
-           match Hashtbl.find_opt t.by_fd fd with
-           | None -> ()
-           | Some curls ->
-               Hashtbl.replace t.by_fd fd
-                 (List.filter (fun c -> c = curl) curls));
+        match Hashtbl.find_opt t.by_fd fd with
+        | None -> ()
+        | Some curls ->
+            Hashtbl.replace t.by_fd fd (List.filter (fun c -> c = curl) curls));
     Curl.Multi.remove t.mt curl;
     Hashtbl.remove t.wakeners curl
 
index be347e1c4bfcba2505d098eff7b6e8dffe6d8d2b..8a234822e6e4e87a9b35c555ab940da5b24e8feb 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Shared code between the individual cohttp-curl clients"
@@ -21,7 +21,7 @@ bug-reports: "https://github.com/mirage/ocaml-cohttp/issues"
 depends: [
   "dune" {>= "3.8"}
   "ocaml" {>= "4.08"}
-  "ocurl" {>= "0.9.2"}
+  "curl" {>= "0.10.0"}
   "http" {= version}
   "stringext"
   "odoc" {with-doc}
index 1f51b0feef6f6ce53ee8f8f1fa60c962153b87ff..b77edac0e9fccfbe0066fd4a095ee95d4c8d9c30 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation with eio backend"
index b1b2fe6f8d695bcb92d66129d4847b4114c6a17b..51543246a88e8c0621454e8639ed6bcf224fd840 100644 (file)
@@ -2,7 +2,7 @@ open Utils
 module IO = Io.IO
 
 type body = Body.t
-type conn = IO.conn * Cohttp.Connection.t [@@warning "-3"]
+type conn = IO.conn * Cohttp.Connection.t
 type writer = Http.Request.t * IO.oc
 type response = writer -> unit
 
index da2eda9f8b4156e892c149243e524d742e622d48..e31bbf513bfb2c1d3240ecef9a5b391f95f23ed0 100644 (file)
@@ -109,8 +109,7 @@ let () =
          https://www.rfc-editor.org/rfc/rfc2606 *)
     let uri = Uri.of_string "http://foo.invalid" in
     match Cohttp_eio.Client.get ~sw client uri with
-    | exception Failure _
-    | exception Eio.Io (Eio.Net.E _, _) ->
+    | (exception Failure _) | (exception Eio.Io (Eio.Net.E _, _)) ->
         (* This should fail, since we are not using the proxy *)
         ()
     | unexepcted_resp, _ ->
index f4b8a2d6b913d46eae333d6b401159fbb9443770..cd2446bb51634b4a2c36c191dafbd5246b0d0fd0 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for the Js_of_ocaml JavaScript compiler"
index 8f1644f201e3c691bfaa1ad8f045f8fde77f5c81..8dee68930f591408024155bc81170a110aad8f04 100644 (file)
@@ -273,15 +273,15 @@ module Make_client_async (P : Params) = Make_api (struct
 
     (* perform call *)
     (match body with
-    | None -> Lwt.return xml##(send Js.null)
-    | Some body ->
-        CLB.to_string body >>= fun body ->
-        let bs = binary_string body in
-        (*Js.Opt.case (File.CoerceTo.blob (Obj.magic blob))
+      | None -> Lwt.return xml##(send Js.null)
+      | Some body ->
+          CLB.to_string body >>= fun body ->
+          let bs = binary_string body in
+          (*Js.Opt.case (File.CoerceTo.blob (Obj.magic blob))
           (fun () -> failwith "could not coerce to blob")
           (fun blob -> Lwt.return (xml##(send_blob blob)))*)
-        (*Lwt.return (xml##send (Js.Opt.return bs)) *)
-        Lwt.return (xml##send (Js.Opt.return (Obj.magic bs))))
+          (*Lwt.return (xml##send (Js.Opt.return bs)) *)
+          Lwt.return (xml##send (Js.Opt.return (Obj.magic bs))))
     >>= fun () ->
     Lwt.on_cancel res (fun () -> xml##abort);
     (* unwrap the response *)
@@ -321,11 +321,11 @@ module Make_client_sync (P : Params) = Make_api (struct
     in
     (* perform call *)
     (match body with
-    | None -> Lwt.return xml##(send Js.null)
-    | Some body ->
-        CLB.to_string body >|= fun body ->
-        let bs = binary_string body in
-        xml##(send (Js.Opt.return (Obj.magic bs))))
+      | None -> Lwt.return xml##(send Js.null)
+      | Some body ->
+          CLB.to_string body >|= fun body ->
+          let bs = binary_string body in
+          xml##(send (Js.Opt.return (Obj.magic bs))))
     >>= fun _body ->
     let body = Bb.construct_body xml in
     (* (re-)construct the response *)
index 0abfc0994368fb6d177c0eab88c1d47528f3eeb8..e9dbe06c0da0a92a469df6b5c3e0e59a3c34bd37 100644 (file)
@@ -22,8 +22,8 @@
     custom reporter or use:
 
     {[
-      let reporter = Logs_browser.console_reporter () in
-      Logs.set_reporter reporter
+    let reporter = Logs_browser.console_reporter () in
+    Logs.set_reporter reporter
     ]} *)
 
 (** Configuration parameters for the XmlHttpRequest engines *)
index e99b72adee4e553d7ac4ad466051753e001b2a5b..3d4bae46ac907320a55eb2f024dae465896363a8 100644 (file)
@@ -18,17 +18,17 @@ let promise_of_lwt lwt =
 
 let () =
   Js.export_all
-    (object%js
-       method request uri =
-         let f () =
-           let uri = Uri.of_string (Js.to_string uri) in
-           let* response, body = Client.get uri in
-           let+ body = Cohttp_lwt.Body.to_string body in
-           let status =
-             Http.Response.status response |> Cohttp.Code.code_of_status
-           in
-           Js.array
-             [| Js.Unsafe.inject status; Js.Unsafe.inject @@ Js.string body |]
-         in
-         promise_of_lwt f
-    end)
+    object%js
+      method request uri =
+        let f () =
+          let uri = Uri.of_string (Js.to_string uri) in
+          let* response, body = Client.get uri in
+          let+ body = Cohttp_lwt.Body.to_string body in
+          let status =
+            Http.Response.status response |> Cohttp.Code.code_of_status
+          in
+          Js.array
+            [| Js.Unsafe.inject status; Js.Unsafe.inject @@ Js.string body |]
+        in
+        promise_of_lwt f
+    end
index 711dfce4455f4b4cf8d766dd9dae44a9e8a745f6..2538a479bf550d243343bf418355664b877580b1 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Command-line utilities shipped with cohttp-lwt-unix"
index 2e970d2039c13de093f61c4ee306ed4cb23df86f..52a6beeb9ab93f1dc34551a2be81dc6abf3e00a3 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for Unix and Windows using Lwt"
index 72cadaf8948f589e9297441d98411ce0c24400ea..d9fdab272b201078d3b2e73eb15a2617474f7214 100644 (file)
@@ -55,16 +55,15 @@ let serve ~info ~docroot ~index uri path =
             | false ->
                 ls_dir file_name
                 >>= Lwt_list.map_s (fun f ->
-                        let file_name = file_name / f in
-                        Lwt.try_bind
-                          (fun () -> Lwt_unix.LargeFile.stat file_name)
-                          (fun stat ->
-                            Lwt.return
-                              ( Some
-                                  (kind_of_unix_kind stat.Unix.LargeFile.st_kind),
-                                stat.Unix.LargeFile.st_size,
-                                f ))
-                          (fun _exn -> Lwt.return (None, 0L, f)))
+                    let file_name = file_name / f in
+                    Lwt.try_bind
+                      (fun () -> Lwt_unix.LargeFile.stat file_name)
+                      (fun stat ->
+                        Lwt.return
+                          ( Some (kind_of_unix_kind stat.Unix.LargeFile.st_kind),
+                            stat.Unix.LargeFile.st_size,
+                            f ))
+                      (fun _exn -> Lwt.return (None, 0L, f)))
                 >>= fun listing ->
                 let body = html_of_listing uri path (sort listing) info in
                 Server.respond_string ~status:`OK ~body ())
index 0293003deb25c86e8268865d1ca39d5e583525eb..36f6269f38d1dc4d846513b9f40201478f6608e7 100644 (file)
@@ -103,8 +103,8 @@ let main ~proxy ~uri ~credential () =
             getenv_opt "http_proxy";
           ]
           |> List.filter_map (function
-               | Some (k, v) -> Some (String.(sub k 0 (rindex k '_')), v)
-               | n -> n) )
+            | Some (k, v) -> Some (String.(sub k 0 (rindex k '_')), v)
+            | n -> n) )
     | v -> (v, None, [])
   in
 
index d14e7124d37f6a839cfe335dbd305426cd4f1567..447a5cc284fcad741dc544e9b5da561ab3487d72 100644 (file)
@@ -22,9 +22,9 @@ val default_reporter : Logs.reporter
     stderr, using coloured output if possible.
 
     {[
-      Fmt_tty.setup_std_outputs ();
-      Logs.set_level ~all:true (Some level);
-      Logs.set_reporter Debug.default_reporter
+    Fmt_tty.setup_std_outputs ();
+    Logs.set_level ~all:true (Some level);
+    Logs.set_reporter Debug.default_reporter
     ]} *)
 
 val activate_debug : unit -> unit
@@ -41,12 +41,12 @@ val debug_active : unit -> bool
     over the various modules logs names as follows.
 
     {[
-      (* Set log level v for all loggers, this does also affect cohttp internal loggers *)
-      Logs.set_level ~all:true level;
-      (* Disable all cohttp-lwt and cohttp-lwt-unix logs *)
-      List.iter (fun src ->
-          match Logs.Src.name src with
-          | "cohttp.lwt.io" | "cohttp.lwt.server" -> Logs.Src.set_level src None
-          | _ -> ())
-      @@ Logs.Src.list ()
+    (* Set log level v for all loggers, this does also affect cohttp internal loggers *)
+    Logs.set_level ~all:true level;
+    (* Disable all cohttp-lwt and cohttp-lwt-unix logs *)
+    List.iter (fun src ->
+        match Logs.Src.name src with
+        | "cohttp.lwt.io" | "cohttp.lwt.server" -> Logs.Src.set_level src None
+        | _ -> ())
+    @@ Logs.Src.list ()
     ]} *)
index e3643e579a0375c60b497de10c8e94a65e7cd2d9..d4c28244611da7a70ab4ba7ae38db3f0f7046e88 100644 (file)
@@ -33,7 +33,7 @@ val create :
     To create a simple HTTP server listening on port 8089:
 
     {[
-      let run = create (`TCP 8080)
+    let run = create (`TCP 8080)
     ]}
 
     When provided, the [stop] thread will terminate the server if it ever
index d399ea15031f66cc53aca990745f524f2b85d21b..096f32a5bad8c8f47f318fc8893ecafb867c70e2 100644 (file)
@@ -52,19 +52,19 @@ let test_server_s ?port ?(name = "Cohttp Server Test") spec f =
       let results =
         tests
         |> Lwt_list.map_s (fun (name, test) ->
-               Logs.info (fun f -> f "Running %s" name);
-               let res =
-                 Lwt.try_bind test
-                   (fun () -> return `Ok)
-                   (fun exn -> return (`Exn exn))
-               in
-               res >|= fun res -> (name, res))
+            Logs.info (fun f -> f "Running %s" name);
+            let res =
+              Lwt.try_bind test
+                (fun () -> return `Ok)
+                (fun exn -> return (`Exn exn))
+            in
+            res >|= fun res -> (name, res))
       in
       results >|= fun results ->
       let ounit_tests =
         results
         |> List.map (fun (name, res) ->
-               name >:: fun () -> match res with `Ok -> () | `Exn x -> raise x)
+            name >:: fun () -> match res with `Ok -> () | `Exn x -> raise x)
       in
       name >::: ounit_tests)
 
index 47d7f6e24bc013044b5eedca26e045feecd4476e..f3462e2596ac4c58f78c387f82c1156215b632f4 100644 (file)
@@ -21,27 +21,29 @@ let test_server tests =
     let uri = Request.uri req in
     let path = Uri.path uri in
     (match Request.meth req with
-    | `PUT ->
-        let status = if Hashtbl.mem store path then `Created else `No_content in
-        Hashtbl.replace store path body;
-        Server.respond_string ~status ~body:"" ()
-    | `DELETE ->
-        if Hashtbl.mem store path then (
-          Hashtbl.remove store path;
-          Server.respond_string ~status:`No_content ~body:"" ())
-        else Server.respond_not_found ~uri ()
-    | `HEAD ->
-        if Hashtbl.mem store path then
-          Server.respond_string ~body:"" ~status:`OK ()
-        else Server.respond_string ~body:"" ~status:`Not_found ()
-    | `GET -> (
-        match Hashtbl.find_opt store path with
-        | Some body -> Server.respond_string ~status:`OK ~body ()
-        | None -> Server.respond_not_found ~uri ())
-    | meth ->
-        Server.respond_string ~status:`Method_not_allowed
-          ~body:("Unsupported method " ^ Code.string_of_method meth)
-          ())
+      | `PUT ->
+          let status =
+            if Hashtbl.mem store path then `Created else `No_content
+          in
+          Hashtbl.replace store path body;
+          Server.respond_string ~status ~body:"" ()
+      | `DELETE ->
+          if Hashtbl.mem store path then (
+            Hashtbl.remove store path;
+            Server.respond_string ~status:`No_content ~body:"" ())
+          else Server.respond_not_found ~uri ()
+      | `HEAD ->
+          if Hashtbl.mem store path then
+            Server.respond_string ~body:"" ~status:`OK ()
+          else Server.respond_string ~body:"" ~status:`Not_found ()
+      | `GET -> (
+          match Hashtbl.find_opt store path with
+          | Some body -> Server.respond_string ~status:`OK ~body ()
+          | None -> Server.respond_not_found ~uri ())
+      | meth ->
+          Server.respond_string ~status:`Method_not_allowed
+            ~body:("Unsupported method " ^ Code.string_of_method meth)
+            ())
     >|= Cohttp_lwt_unix_test.response
   in
   Cohttp_lwt_unix_test.test_server_s ~name:"mutable resources" spec tests
index 5c8202fd2333043c937c9fa9f957a72bd44bc78e..0dddcd81edac2fb1df8abb61be21c8ef6a0c9278 100644 (file)
@@ -38,13 +38,13 @@ let server =
         ();
     ]
   @ List.init (leak_repeat * 2) (fun i _ _ ->
-        (* no leaks *)
-        if i mod 2 = 0 then
-          Server.respond_string ~status:`OK ~body:"" () >|= fun rsp ->
-          `Response rsp
-        else
-          Server.respond_string ~status:`OK ~body:"no leak" () >|= fun rsp ->
-          `Response rsp)
+      (* no leaks *)
+      if i mod 2 = 0 then
+        Server.respond_string ~status:`OK ~body:"" () >|= fun rsp ->
+        `Response rsp
+      else
+        Server.respond_string ~status:`OK ~body:"no leak" () >|= fun rsp ->
+        `Response rsp)
   (* pipelined_expert *)
   @ [
       (fun _ _ ->
index e54b83c94c42ae4733e04af17648d8a9bc79a31a..0e07bc5105a2a4af82ed6d4636b77696ccf21021 100644 (file)
@@ -45,8 +45,7 @@ let ts_noisy =
         Client.get ~ctx uri >>= fun (resp, body) ->
         assert_equal (Response.status resp) `Not_modified;
         let headers = Response.headers resp in
-        assert_equal ~printer:Transfer.string_of_encoding
-          Transfer.(Fixed 0L)
+        assert_equal ~printer:Transfer.string_of_encoding Transfer.Unknown
           (Header.get_transfer_encoding headers);
         body |> Body.is_empty >|= fun is_empty ->
         assert_bool "No body returned when not modified" is_empty
index cffc3eada0bad556f2f1f5cf3b140ee1ab2ba1e9..89b04556cdc4fc26d2a0e917f46b5e5ce27e959e 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation using the Lwt concurrency library"
index 0c454a724cfbfa186f37a0df49fcbbdbf7cd6267..0a53dcf77c63e70f24d47ba5b8c428260b2bb825 100644 (file)
@@ -203,18 +203,18 @@ module Make (Net : S.Net) : S.Connection with module Net = Net = struct
 
         (* select encoding based on (1st) header or (2nd) body *)
         (match Header.get_transfer_encoding headers with
-        | Unknown -> (
-            match Body.transfer_encoding body with
-            | Fixed _ as e -> Lwt.return (e, body)
-            | Chunked as e when connection.persistent = `True ->
-                Lwt.return (e, body)
-            | Chunked (* connection.persistent <> `True *) ->
-                (* We don't know yet whether chunked encoding is supported.
-                 * Therefore use fixed length encoding. *)
-                Body.length body >>= fun (length, body) ->
-                Lwt.return (Cohttp.Transfer.Fixed length, body)
-            | Unknown -> assert false)
-        | e -> Lwt.return (e, body))
+          | Unknown -> (
+              match Body.transfer_encoding body with
+              | Fixed _ as e -> Lwt.return (e, body)
+              | Chunked as e when connection.persistent = `True ->
+                  Lwt.return (e, body)
+              | Chunked (* connection.persistent <> `True *) ->
+                  (* We don't know yet whether chunked encoding is supported.
+                   * Therefore use fixed length encoding. *)
+                  Body.length body >>= fun (length, body) ->
+                  Lwt.return (Cohttp.Transfer.Fixed length, body)
+              | Unknown -> assert false)
+          | e -> Lwt.return (e, body))
         >>= fun (encoding, body) ->
         let headers =
           if
index 09c803d5b7d16b878657ff74d97ffdfed91134b7..822b04d5399d266287adf422d904176fa84735b8 100644 (file)
@@ -23,8 +23,8 @@ module Make_no_cache (Connection : S.Connection) = struct
       (fun () ->
         res >>= fun (_, body) ->
         (match body with
-        | `Empty | `String _ | `Strings _ -> Lwt.return_unit
-        | `Stream stream -> Lwt_stream.closed stream)
+          | `Empty | `String _ | `Strings _ -> Lwt.return_unit
+          | `Stream stream -> Lwt_stream.closed stream)
         >>= fun () ->
         Connection.close connection;
         Lwt.return_unit)
@@ -301,11 +301,11 @@ end = struct
 
   let call self ?headers ?body ?absolute_form meth uri =
     (match self.proxy_endp with
-    | None ->
-        Net.resolve ~ctx:self.ctx self.proxy_uri >>= fun proxy_endp ->
-        self.proxy_endp <- Some proxy_endp;
-        Lwt.return proxy_endp
-    | Some proxy_endp -> Lwt.return proxy_endp)
+      | None ->
+          Net.resolve ~ctx:self.ctx self.proxy_uri >>= fun proxy_endp ->
+          self.proxy_endp <- Some proxy_endp;
+          Lwt.return proxy_endp
+      | Some proxy_endp -> Lwt.return proxy_endp)
     >>= fun proxy_endp ->
     let remote_port =
       match Uri_services.tcp_port_of_uri uri with
index 55d71998e905c8f291fb595e834da62ab9208a28..0f52a6882288e939234779a09e74a57581fe2323 100644 (file)
@@ -99,8 +99,8 @@ type call =
     follows in [cohttp-lwt-unix]
 
     {[
-      Cohttp_lwt_unix.Debug.activate_debug ();
-      Logs.set_level (Some Logs.Warning)
+    Cohttp_lwt_unix.Debug.activate_debug ();
+    Logs.set_level (Some Logs.Warning)
     ]}
 
     @raise {!Connection.Retry}
index 397b80ed9027b3a3ae51e0c9d8579b4ec37e230b..8b89c5b233b2a1c0cbf9a99c80bb189afbc6c91c 100644 (file)
@@ -1,6 +1,6 @@
 open Lwt.Infix
 module Header = Cohttp.Header
-module Connection = Cohttp.Connection [@@warning "-3"]
+module Connection = Cohttp.Connection
 
 module Make (IO : S.IO) = struct
   module IO = IO
index 11300904b824f115b640c1314edec583b7583fcd..78581334420b24fdc3f65a03ad41e80da259a898 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP implementation for the MirageOS unikernel"
@@ -34,14 +34,14 @@ depends: [
   "mirage-channel" {>= "4.0.0"}
   "conduit" {>= "8.0.0"}
   "conduit-mirage" {>= "8.0.0"}
-  "mirage-kv" {>= "3.0.0"}
+  "mirage-kv" {>= "6.0.1"}
   "lwt" {>= "2.4.3"}
   "cohttp-lwt" {= version}
   "cstruct" {>= "6.0.0"}
   "fmt" {>= "0.8.7"}
-  "astring"
   "magic-mime"
   "ppx_sexp_conv" {>= "v0.13.0"}
+  "alcotest" {with-test & >= "1.7.0"}
   "cohttp" {= version}
   "odoc" {with-doc}
 ]
diff --git a/cohttp-mirage/examples/dune b/cohttp-mirage/examples/dune
new file mode 100644 (file)
index 0000000..e60f3eb
--- /dev/null
@@ -0,0 +1,11 @@
+(executable
+ (name static_server)
+ (modules static_server)
+ (libraries
+  cohttp-mirage
+  cohttp-lwt-unix
+  mirage-kv
+  optint
+  lwt.unix
+  logs.fmt
+  fmt.tty))
diff --git a/cohttp-mirage/examples/static_server.ml b/cohttp-mirage/examples/static_server.ml
new file mode 100644 (file)
index 0000000..f97655f
--- /dev/null
@@ -0,0 +1,198 @@
+open Lwt.Infix
+
+module Dir : sig
+  include Mirage_kv.RO
+
+  val connect : string -> t
+end = struct
+  type t = { root : string }
+  type key = Mirage_kv.Key.t
+  type error = Mirage_kv.error
+
+  let pp_error = Mirage_kv.pp_error
+  let connect root = { root }
+  let disconnect _ = Lwt.return_unit
+
+  let path t key =
+    let segment s =
+      match s with
+      | "" | "." | ".." -> None
+      | s when String.exists (function '/' | '\000' -> true | _ -> false) s ->
+          None
+      | s -> Some s
+    in
+    let rec go acc = function
+      | [] -> Some acc
+      | s :: rest -> (
+          match segment s with
+          | None -> None
+          | Some s -> go (Filename.concat acc s) rest)
+    in
+    go t.root (Mirage_kv.Key.segments key)
+
+  let stat t key =
+    match path t key with
+    | None -> Lwt.return None
+    | Some path ->
+        Lwt.catch
+          (fun () -> Lwt_unix.stat path >|= fun stat -> Some (path, stat))
+          (function
+            | Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
+                Lwt.return None
+            | e -> Lwt.reraise e)
+
+  let get t key =
+    stat t key >>= function
+    | Some (path, { Unix.st_kind = Unix.S_REG; _ }) ->
+        Lwt_io.with_file ~mode:Lwt_io.Input path Lwt_io.read >|= Result.ok
+    | Some _ -> Lwt.return (Error (`Value_expected key))
+    | None -> Lwt.return (Error (`Not_found key))
+
+  let exists t key =
+    stat t key >|= function
+    | Some (_, { Unix.st_kind = Unix.S_REG; _ }) -> Ok (Some `Value)
+    | Some (_, { Unix.st_kind = Unix.S_DIR; _ }) -> Ok (Some `Dictionary)
+    | Some _ | None -> Ok None
+
+  let size t key =
+    stat t key >|= function
+    | Some (_, { Unix.st_size = size; _ }) -> Ok (Optint.Int63.of_int size)
+    | None -> Error (`Not_found key)
+
+  let list t key =
+    match path t key with
+    | None -> Lwt.return (Error (`Not_found key))
+    | Some dir ->
+        Lwt.catch
+          (fun () ->
+            Lwt_unix.files_of_directory dir
+            |> Lwt_stream.to_list
+            >>= Lwt_list.filter_map_s (fun name ->
+                if name = "." || name = ".." then Lwt.return_none
+                else
+                  Lwt_unix.stat (Filename.concat dir name)
+                  >|= fun { Unix.st_kind; _ } ->
+                  match st_kind with
+                  | Unix.S_REG -> Some (Mirage_kv.Key.add key name, `Value)
+                  | Unix.S_DIR -> Some (Mirage_kv.Key.add key name, `Dictionary)
+                  | _ -> None)
+            >|= Result.ok)
+          (function
+            | Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
+                Lwt.return (Error (`Not_found key))
+            | e -> Lwt.reraise e)
+
+  let get_partial _ key ~offset:_ ~length:_ =
+    Lwt.return (Error (`Not_found key))
+
+  let last_modified _ key = Lwt.return (Error (`Not_found key))
+  let digest _ key = Lwt.return (Error (`Not_found key))
+end
+
+let html_escape s =
+  let b = Buffer.create (String.length s) in
+  String.iter
+    (function
+      | '&' -> Buffer.add_string b "&amp;"
+      | '<' -> Buffer.add_string b "&lt;"
+      | '>' -> Buffer.add_string b "&gt;"
+      | '"' -> Buffer.add_string b "&quot;"
+      | '\'' -> Buffer.add_string b "&#39;"
+      | c -> Buffer.add_char b c)
+    s;
+  Buffer.contents b
+
+let html_of_listing dir entries =
+  let segments = Mirage_kv.Key.segments dir in
+  let link segments text =
+    let href =
+      segments
+      |> List.map (Uri.pct_encode ~component:`Path)
+      |> String.concat "/"
+      |> html_escape
+    in
+    Printf.sprintf {|<li><a href="/%s">%s</a></li>|} href (html_escape text)
+  in
+  let parent =
+    match List.rev segments with
+    | [] -> []
+    | _ :: up -> [ link (List.rev up) "../" ]
+  in
+  let entry (key, kind) =
+    let name = Mirage_kv.Key.basename key in
+    let text = match kind with `Dictionary -> name ^ "/" | `Value -> name in
+    (name, link (segments @ [ name ]) text)
+  in
+  let entries =
+    List.map entry entries
+    |> List.sort (fun (a, _) (b, _) -> String.compare a b)
+    |> List.map snd
+  in
+  let title = "Index of " ^ html_escape (Mirage_kv.Key.to_string dir) in
+  String.concat "\n"
+    ([
+       "<!DOCTYPE html>";
+       "<html>";
+       "<head><title>" ^ title ^ "</title></head>";
+       "<body>";
+       "<h1>" ^ title ^ "</h1>";
+       "<ul>";
+     ]
+    @ parent
+    @ entries
+    @ [ "</ul>"; "</body>"; "</html>"; "" ])
+
+module Generated_index (KV : Mirage_kv.RO) : Mirage_kv.RO with type t = KV.t =
+struct
+  include KV
+
+  let index = "index.html"
+
+  let generates t key =
+    if Mirage_kv.Key.basename key <> index then Lwt.return_false
+    else
+      KV.exists t (Mirage_kv.Key.parent key) >|= function
+      | Ok (Some `Dictionary) -> true
+      | Ok (Some `Value | None) | Error _ -> false
+
+  let get t key =
+    KV.get t key >>= function
+    | Ok _ as ok -> Lwt.return ok
+    | Error _ as error -> (
+        generates t key >>= function
+        | false -> Lwt.return error
+        | true -> (
+            let dir = Mirage_kv.Key.parent key in
+            KV.list t dir >|= function
+            | Ok entries -> Ok (html_of_listing dir entries)
+            | Error _ -> error))
+
+  let exists t key =
+    KV.exists t key >>= function
+    | Ok None -> (
+        generates t key >|= function
+        | true -> Ok (Some `Value)
+        | false -> Ok None)
+    | r -> Lwt.return r
+end
+
+module Store = Generated_index (Dir)
+module Server = Cohttp_lwt_unix.Server
+module Static = Cohttp_mirage.Static.HTTP (Store) (Server)
+
+let request_fn uri headers =
+  Cohttp.Header.add headers "x-served-by"
+    (Printf.sprintf "cohttp-mirage static (%s)" (Uri.path uri))
+
+let () =
+  let docroot = if Array.length Sys.argv > 1 then Sys.argv.(1) else "htdocs" in
+  let port =
+    if Array.length Sys.argv > 2 then int_of_string Sys.argv.(2) else 8080
+  in
+  Fmt_tty.setup_std_outputs ();
+  Logs.set_level (Some Logs.Info);
+  Logs.set_reporter (Logs_fmt.reporter ());
+  Logs.info (fun f -> f "serving %s on http://0.0.0.0:%d/" docroot port);
+  Lwt_main.run
+    (Static.start ~http_port:port ~request_fn (Dir.connect docroot)
+       (fun (`TCP port) spec -> Server.create ~mode:(`TCP (`Port port)) spec))
index 5644293876b29aab7cdccaf5c3797f28abe92b4c..0cd077ca2c9f05a47d9ac8fcdf99753d0ae66a98 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.2.2
+ * cohttp v6.3.0
  *)
 
 module Make (R : Resolver_mirage.S) (S : Conduit_mirage.S) = struct
index a318811a0ece43d1dfe57234cc5769f05cc911f2..7e2d867e78a9fb7c339910a6c97e5743f866141c 100644 (file)
@@ -10,5 +10,4 @@
   mirage-channel
   mirage-kv
   mirage-flow
-  magic-mime
-  astring))
+  magic-mime))
index f5124f137c23434b009f230fa8499c17f0c931ec..8267c3460a99818932fb5d65d15bebe185b6276e 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.2.2
+ * cohttp v6.3.0
  *)
 
 open Lwt.Infix
index b39714969d7c3eb96f547c4b80562894323ccc74..6689947023710163d1c8ad726be21fef27dc5ee1 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.2.2
+ * cohttp v6.3.0
  *)
 
 (** Cohttp IO implementation using Mirage channels. *)
index 77a4f5c81f4b0a936314a264b51d0cf56e7f8a32..c3b33afb1dd3407ca80823bd0919be0e88fcf009 100644 (file)
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.2.2
+ * cohttp v6.3.0
  *)
 
 module Key = Mirage_kv.Key
-module Connection = Cohttp.Connection [@@warning "-3"]
+module Connection = Cohttp.Connection
 
 module HTTP (FS : Mirage_kv.RO) (S : Cohttp_lwt.S.Server) = struct
   open Lwt.Infix
-  open Astring
 
   let failf fmt = Fmt.failwith fmt
 
@@ -38,34 +37,27 @@ module HTTP (FS : Mirage_kv.RO) (S : Cohttp_lwt.S.Server) = struct
     | Error e -> Fmt.failwith "exists %a" FS.pp_error e
 
   let dispatcher request_fn =
-    let rec fn fs uri =
-      match Uri.path uri with
-      | ("" | "/") as path ->
-          Logs.info (fun f -> f "request for '%s'" path);
-          fn fs (Uri.with_path uri "index.html")
-      | path when String.is_suffix ~affix:"/" path ->
-          Logs.info (fun f -> f "request for '%s'" path);
-          fn fs (Uri.with_path uri "index.html")
-      | path ->
-          Logs.info (fun f -> f "request for '%s'" path);
-          Lwt.catch
-            (fun () ->
-              read_fs fs path >>= fun body ->
-              let mime_type = Magic_mime.lookup path in
-              let headers = Cohttp.Header.init_with "content-type" mime_type in
-              let headers =
-                match request_fn with
-                | None -> headers
-                | Some fn -> fn uri headers
-              in
-              S.respond_string ~status:`OK ~body ~headers ())
-            (fun _exn ->
-              let with_index = Fmt.str "%s/index.html" path in
-              exists fs with_index >>= function
-              | true -> fn fs (Uri.with_path uri with_index)
-              | false -> S.respond_not_found ())
+    let respond uri path body =
+      let mime_type = Magic_mime.lookup path in
+      let headers = Cohttp.Header.init_with "content-type" mime_type in
+      let headers =
+        match request_fn with None -> headers | Some fn -> fn uri headers
+      in
+      S.respond_string ~status:`OK ~body ~headers ()
     in
-    fn
+    fun fs uri ->
+      let path =
+        match Cohttp.Path.normalise uri with "" -> "index.html" | path -> path
+      in
+      Logs.info (fun f -> f "request for %S" path);
+      Lwt.try_bind
+        (fun () -> read_fs fs path)
+        (respond uri path)
+        (fun _exn ->
+          let with_index = path ^ "/index.html" in
+          exists fs with_index >>= function
+          | true -> read_fs fs with_index >>= respond uri with_index
+          | false -> S.respond_not_found ())
 
   let start ~http_port ?request_fn fs http =
     let callback (_, cid) request _body =
index 4d2d2cec438e7c655706cf88e2fe7106290e8540..1ff2752473aee32137331a9dbe4d8685d0077529 100644 (file)
@@ -14,7 +14,7 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- * cohttp v6.2.2
+ * cohttp v6.3.0
  *)
 
 (** Serve static HTTP sites from a Mirage key-value store. *)
diff --git a/cohttp-mirage/test/dune b/cohttp-mirage/test/dune
new file mode 100644 (file)
index 0000000..b3be3bf
--- /dev/null
@@ -0,0 +1,10 @@
+(executable
+ (name test_static)
+ (modules test_static)
+ (libraries cohttp-mirage cohttp-lwt cohttp http lwt mirage-kv uri alcotest))
+
+(rule
+ (alias runtest)
+ (package cohttp-mirage)
+ (action
+  (run ./test_static.exe)))
diff --git a/cohttp-mirage/test/test_static.ml b/cohttp-mirage/test/test_static.ml
new file mode 100644 (file)
index 0000000..3df207b
--- /dev/null
@@ -0,0 +1,214 @@
+(* Tests for [Cohttp_mirage.Static.HTTP]. *)
+
+let run p =
+  match Lwt.state p with
+  | Lwt.Return v -> v
+  | Lwt.Fail e -> raise e
+  | Lwt.Sleep -> Alcotest.fail "promise did not resolve"
+
+(* An in-memory [Mirage_kv.RO] over a list of (key, contents) pairs. *)
+module Mock_fs : sig
+  include Mirage_kv.RO
+
+  val create : (string * string) list -> t
+end = struct
+  type t = (string * string) list
+  type key = Mirage_kv.Key.t
+  type error = Mirage_kv.error
+
+  let pp_error = Mirage_kv.pp_error
+  let create entries = entries
+  let disconnect _ = Lwt.return_unit
+  let path key = String.concat "/" (Mirage_kv.Key.segments key)
+
+  let is_dictionary t path =
+    let prefix = path ^ "/" in
+    List.exists (fun (k, _) -> String.starts_with ~prefix k) t
+
+  let get t key =
+    let path = path key in
+    match List.assoc_opt path t with
+    | Some contents -> Lwt.return (Ok contents)
+    | None when is_dictionary t path -> Lwt.return (Error (`Value_expected key))
+    | None -> Lwt.return (Error (`Not_found key))
+
+  let exists t key =
+    let path = path key in
+    if List.mem_assoc path t then Lwt.return (Ok (Some `Value))
+    else if is_dictionary t path then Lwt.return (Ok (Some `Dictionary))
+    else Lwt.return (Ok None)
+
+  let unsupported _ = Alcotest.fail "unexpected key-value store operation"
+  let get_partial _ _ ~offset:_ ~length:_ = unsupported ()
+  let list _ _ = unsupported ()
+  let last_modified _ _ = unsupported ()
+  let digest _ _ = unsupported ()
+  let size _ _ = unsupported ()
+end
+
+module Mock_server : sig
+  include Cohttp_lwt.S.Server
+
+  val serve : t -> string -> (Http.Response.t * Cohttp_lwt.Body.t) Lwt.t
+end = struct
+  module IO = struct
+    type 'a t = 'a Lwt.t
+
+    let ( >>= ) = Lwt.bind
+    let return = Lwt.return
+
+    type ic = unit
+    type oc = unit
+    type conn = unit
+    type error = |
+
+    let refill () = Lwt.return `Eof
+    let with_input_buffer () ~f = fst (f "" ~pos:0 ~len:0)
+    let read_line () = Lwt.return None
+    let read () _ = Lwt.return ""
+    let write () _ = Lwt.return_unit
+    let flush () = Lwt.return_unit
+    let catch f = Lwt.map Result.ok (f ())
+    let pp_error _ = function (_ : error) -> .
+  end
+
+  type body = Cohttp_lwt.Body.t
+  type conn = IO.conn * Cohttp.Connection.t [@@warning "-3"]
+  type response = Http.Response.t * body
+
+  type response_action =
+    [ `Expert of Http.Response.t * (IO.ic -> IO.oc -> unit IO.t)
+    | `Response of response ]
+
+  type t = { handler : conn -> Http.Request.t -> body -> response Lwt.t }
+
+  let make ?conn_closed:_ ~callback () = { handler = callback }
+  let unsupported _ = Alcotest.fail "unexpected server operation"
+  let make_response_action ?conn_closed:_ ~callback:_ () = unsupported ()
+  let make_expert ?conn_closed:_ ~callback:_ () = unsupported ()
+  let callback _ _ _ _ = unsupported ()
+
+  let resolve_local_file ~docroot ~uri =
+    Cohttp.Path.resolve_local_file ~docroot ~uri
+
+  let respond ?(headers = Http.Header.init ()) ~status ~body () =
+    Lwt.return (Http.Response.make ~status ~headers (), body)
+
+  let respond_string ?headers ~status ~body () =
+    respond ?headers ~status ~body:(`String body) ()
+
+  let respond_error ?headers ?(status = `Internal_server_error) ~body () =
+    respond_string ?headers ~status ~body ()
+
+  let respond_redirect ?(headers = Http.Header.init ()) ~uri () =
+    let headers = Http.Header.add headers "location" (Uri.to_string uri) in
+    respond_string ~headers ~status:`Found ~body:"" ()
+
+  let respond_need_auth ?(headers = Http.Header.init ()) ~auth () =
+    let headers = Cohttp.Header.add_authorization_req headers auth in
+    respond_string ~headers ~status:`Unauthorized ~body:"" ()
+
+  let respond_not_found ?uri:_ () =
+    respond_string ~status:`Not_found ~body:"Not found" ()
+
+  let serve t target =
+    let request = Http.Request.make ~meth:`GET target in
+    t.handler ((), (Cohttp.Connection.create () [@warning "-3"])) request `Empty
+end
+
+module Static = Cohttp_mirage.Static.HTTP (Mock_fs) (Mock_server)
+
+let store =
+  Mock_fs.create
+    [
+      ("index.html", "root index");
+      ("foo/index.html", "foo index");
+      ("foo/bar/index.html", "foo/bar index");
+      ("style.css", "css body");
+      ("my file.txt", "spaced");
+    ]
+
+let request ?request_fn target =
+  let t = Static.start ~http_port:8080 ?request_fn store (fun _ t -> t) in
+  let response, body = run (Mock_server.serve t target) in
+  (response, run (Cohttp_lwt.Body.to_string body))
+
+let check_served name ~target ~expected =
+  let response, body = request target in
+  Alcotest.(check int)
+    (name ^ ": status") 200
+    (Http.Status.to_int (Http.Response.status response));
+  Alcotest.(check string) (name ^ ": body") expected body
+
+let check_not_found name ~target =
+  let response, _ = request target in
+  Alcotest.(check int)
+    (name ^ ": status") 404
+    (Http.Status.to_int (Http.Response.status response))
+
+let test_index_pages () =
+  check_served "root" ~target:"/" ~expected:"root index";
+  check_served "directory" ~target:"/foo" ~expected:"foo index";
+  check_served "directory trailing slash" ~target:"/foo/" ~expected:"foo index";
+  check_served "nested directory" ~target:"/foo/bar" ~expected:"foo/bar index";
+  check_served "nested directory trailing slash" ~target:"/foo/bar/"
+    ~expected:"foo/bar index";
+  check_served "empty segments collapsed" ~target:"/foo//bar/"
+    ~expected:"foo/bar index"
+
+let test_files () =
+  check_served "file" ~target:"/style.css" ~expected:"css body";
+  check_not_found "missing file" ~target:"/nope.css";
+  check_not_found "missing directory" ~target:"/nope/"
+
+let test_content_type () =
+  let response, _ = request "/style.css" in
+  Alcotest.(check (option string))
+    "css content-type" (Some "text/css")
+    (Http.Header.get (Http.Response.headers response) "content-type");
+  let response, _ = request "/foo/" in
+  Alcotest.(check (option string))
+    "index content-type is that of the page served, not the directory"
+    (Some "text/html")
+    (Http.Header.get (Http.Response.headers response) "content-type")
+
+let test_traversal_clamped () =
+  check_served "dot-dot" ~target:"/foo/../style.css" ~expected:"css body";
+  check_served "dot-dot above root" ~target:"/../../style.css"
+    ~expected:"css body";
+  check_served "encoded slash" ~target:"/..%2f..%2fstyle.css"
+    ~expected:"css body";
+  check_served "fully encoded" ~target:"/%2e%2e%2f%2e%2e%2fstyle.css"
+    ~expected:"css body";
+  check_served "encoded slash into directory index" ~target:"/foo%2fbar%2f"
+    ~expected:"foo/bar index"
+
+(* Percent-decoding should happens exactly once. *)
+let test_single_decode () =
+  check_served "encoded space" ~target:"/my%20file.txt" ~expected:"spaced";
+  check_not_found "double encoded slash" ~target:"/..%252f..%252fstyle.css";
+  check_not_found "double encoded, fully" ~target:"/%252e%252e%252fstyle.css"
+
+let test_request_fn () =
+  let request_fn uri headers =
+    Http.Header.add headers "x-request-uri" (Uri.path uri)
+  in
+  let response, body = request ~request_fn "/foo/" in
+  Alcotest.(check string) "index still served" "foo index" body;
+  Alcotest.(check (option string))
+    "request_fn sees the request URI" (Some "/foo/")
+    (Http.Header.get (Http.Response.headers response) "x-request-uri")
+
+let () =
+  Alcotest.run "cohttp-mirage static"
+    [
+      ( "paths",
+        [
+          ("index pages", `Quick, test_index_pages);
+          ("files", `Quick, test_files);
+          ("content types", `Quick, test_content_type);
+          ("traversal clamped to root", `Quick, test_traversal_clamped);
+          ("single percent-decode", `Quick, test_single_decode);
+          ("request_fn", `Quick, test_request_fn);
+        ] );
+    ]
index 2e1f87f02a1ad968af62b56983d9cfff18600c33..c3afe96ea39a53740f3508dd1654169e3e5b34bd 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Lightweight Cohttp + Lwt based HTTP server"
index 054ac93d2fc182a7603591710efc53f617d01b98..1d60f23e45402c484cd9dab47316c84054c724ae 100644 (file)
@@ -290,10 +290,10 @@ module Context = struct
       let* () =
         Http.Header.to_list headers
         |> Lwt_list.iter_s (fun (k, v) ->
-               let* () = Lwt_io.write t.oc k in
-               let* () = Lwt_io.write t.oc ": " in
-               let* () = Lwt_io.write t.oc v in
-               Lwt_io.write t.oc "\r\n")
+            let* () = Lwt_io.write t.oc k in
+            let* () = Lwt_io.write t.oc ": " in
+            let* () = Lwt_io.write t.oc v in
+            Lwt_io.write t.oc "\r\n")
       in
       let* () = Lwt_io.write t.oc "\r\n" in
       Body.write body t.oc
index 4eb68097a411847f116fa5fa6c415f460e080838..64a987efba50f899b2f8ff60d11d389b56292578 100644 (file)
     An example server:
 
     {[
-      open Lwt.Syntax
-
-      let server_callback ctx =
-        Lwt.join
-          [
-            Cohttp_server_lwt_unix.ontext.discard_body ctx;
-            Cohttp_server_lwt_unix.ontext.respond ctx (Http.Response.make ())
-              (Cohttp_server_lwt_unix.Body.string "hello world");
-          ]
-
-      let main () =
-        let* _server =
-          let listen_address = Unix.(ADDR_INET (inet_addr_loopback, 8080)) in
-          let server = Cohttp_server_lwt_unix.create server_callback in
-          Lwt_io.establish_server_with_client_address ~backlog:10_000
-            listen_address (fun _addr ch ->
-              Cohttp_server_lwt_unix.handle_connection server ch)
-        in
-        let forever, _ = Lwt.wait () in
-        forever
-
-      let () = ignore (Lwt_main.run (main ()))
+    open Lwt.Syntax
+
+    let server_callback ctx =
+      Lwt.join
+        [
+          Cohttp_server_lwt_unix.ontext.discard_body ctx;
+          Cohttp_server_lwt_unix.ontext.respond ctx (Http.Response.make ())
+            (Cohttp_server_lwt_unix.Body.string "hello world");
+        ]
+
+    let main () =
+      let* _server =
+        let listen_address = Unix.(ADDR_INET (inet_addr_loopback, 8080)) in
+        let server = Cohttp_server_lwt_unix.create server_callback in
+        Lwt_io.establish_server_with_client_address ~backlog:10_000
+          listen_address (fun _addr ch ->
+            Cohttp_server_lwt_unix.handle_connection server ch)
+      in
+      let forever, _ = Lwt.wait () in
+      forever
+
+    let () = ignore (Lwt_main.run (main ()))
     ]} *)
 
 module Body : sig
index e27d0acc2fd8da4078da9c2d13ea38c5cc8b37dc..f36324255e68022516307eb57000cecacfa3db0d 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "CoHTTP toplevel pretty printers for HTTP types"
index f11d47da06c829d33708c1ff6276785e7581e441..77102bdbb12000e34978b13ff7b418e44f27f9b3 100644 (file)
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "An OCaml library for HTTP clients and servers"
index 76f7486e1b9657a3f69ea0ebd0d80159f4d7d7a8..f38a7424249884d5ec042b146d469f216fecbfaf 100644 (file)
@@ -2,7 +2,7 @@ module Accept = Accept
 module Auth = Auth
 module Body = Body
 module Conf = Conf
-module Connection = Connection [@@deprecated "Connection.t values are useless."]
+module Connection = Connection
 module Code = Code
 module Cookie = Cookie
 module Header = Header
index cd614ce4c81c34ca4a47cb5df61a95de776ce55e..eeb21d264cf02b6be04da4935435ca4be5e8c439 100644 (file)
@@ -1,7 +1,12 @@
-let resolve_local_file ~docroot ~uri =
-  let path = Uri.(pct_decode (path (resolve "http" (of_string "/") uri))) in
-  let rel_path =
-    if String.length path > 0 then String.sub path 1 (String.length path - 1)
-    else ""
+let remove_dot_segments path =
+  let rec go acc = function
+    | [] -> List.rev acc
+    | ("" | ".") :: rest -> go acc rest
+    | ".." :: rest -> (
+        match acc with [] -> go [] rest | _ :: tl -> go tl rest)
+    | seg :: rest -> go (seg :: acc) rest
   in
-  Filename.concat docroot rel_path
+  path |> String.split_on_char '/' |> go [] |> String.concat "/"
+
+let normalise uri = uri |> Uri.path |> Uri.pct_decode |> remove_dot_segments
+let resolve_local_file ~docroot ~uri = Filename.concat docroot (normalise uri)
index 601901553379ab492a1fdaf3f616427489691fa9..9a00881c19a54cd61926ddf2ce4d6347fabb64c9 100644 (file)
@@ -1,6 +1,15 @@
 val resolve_local_file : docroot:string -> uri:Uri.t -> string
 (** Resolve the given URI to a local file in the given docroot.
 
-    This decodes and normalises the Uri. It strips out .. characters so that the
-    request will not escape the docroot. The returned filepath is fully
-    qualified iff the given docroot is fully qualified. *)
+    This is [Filename.concat docroot (normalise uri)]. It decodes
+    percent-encoding once and removes [..] segments so that the request cannot
+    escape the docroot. The returned filepath is fully qualified iff the given
+    docroot is fully qualified. *)
+
+val normalise : Uri.t -> string
+(** [normalise uri] returns the path of [uri] with percent-encoding decoded
+    exactly once and [.]/[..] segments resolved, as a relative path that can
+    never ascend above its root.
+
+    Use this to derive a lookup key from a request URI, for example a filesystem
+    path or a key-value store key. *)
index 5f5251b85ee370c72a843b09b24b9ce6b8c526f2..6aa2a199a1fb3f95ec069a2b337479e15f50402c 100644 (file)
@@ -32,15 +32,14 @@ module Forward = struct
             no_proxy
             |> String.split_on_char ','
             |> List.filter_map (fun pattern ->
-                   if pattern = "" then None else Some (String.trim pattern))
+                if pattern = "" then None else Some (String.trim pattern))
             |> List.map (fun pattern ->
-                   match Ipaddr.of_string pattern with
-                   | Ok addr -> Ipaddr_prefix (Ipaddr.Prefix.of_addr addr)
-                   | Error _ -> (
-                       match Ipaddr.Prefix.of_string pattern with
-                       | Ok prefix -> Ipaddr_prefix prefix
-                       | Error _ -> Name (trim_dots ~first_leading:true pattern)
-                       ))
+                match Ipaddr.of_string pattern with
+                | Ok addr -> Ipaddr_prefix (Ipaddr.Prefix.of_addr addr)
+                | Error _ -> (
+                    match Ipaddr.Prefix.of_string pattern with
+                    | Ok prefix -> Ipaddr_prefix prefix
+                    | Error _ -> Name (trim_dots ~first_leading:true pattern)))
           in
           Patterns patterns
 
index bfc532df355c525f76f28507039930c8b4893468..a97b23886d6ad7ca81a18ea0a972622e800cc027 100644 (file)
@@ -40,23 +40,21 @@ let status t = t.status
 let make ?(version = `HTTP_1_1) ?(status = `OK) ?(encoding = Transfer.Unknown)
     ?(headers = Header.init ()) () =
   let headers =
-    match encoding with
-    | Unknown -> (
-        match Header.get_transfer_encoding headers with
-        | Unknown -> Header.add_transfer_encoding headers Chunked
-        | _ -> headers)
-    | _ -> Header.add_transfer_encoding headers encoding
+    if not (Http.Status.body_allowed status) then headers
+    else
+      match encoding with
+      | Unknown -> (
+          match Header.get_transfer_encoding headers with
+          | Unknown -> Header.add_transfer_encoding headers Chunked
+          | _ -> headers)
+      | _ -> Header.add_transfer_encoding headers encoding
   in
   { headers; version; status }
 
 let pp_hum ppf r =
   Format.fprintf ppf "%s" (r |> sexp_of_t |> Sexplib0.Sexp.to_string_hum)
 
-let allowed_body response =
-  (* rfc7230#section-5.7.1 *)
-  match status response with
-  | #Code.informational_status | `No_content | `Not_modified -> false
-  | #Code.status_code -> true
+let allowed_body response = Http.Status.body_allowed (status response)
 
 let has_body response =
   if allowed_body response then Transfer.has_body (encoding response) else `No
index 0c26753222d173235b9b9aae9c70badeb42ca399..c83bf702c8769f1524de4bc4346fc9c7b17437e2 100644 (file)
@@ -2,7 +2,7 @@ module type S = sig
   module IO : S.IO
 
   type body
-  type conn = IO.conn * Connection.t [@@warning "-3"]
+  type conn = IO.conn * Connection.t
   type response
 
   type response_action =
index 090b7864e2da7ca98a63986ec3173223da558d33..ca47fa8b07b6f2275d91598c848bc0b02fca5756 100644 (file)
  (package cohttp)
  (action
   (run ./test_proxy.exe)))
+
+(executable
+ (name test_response)
+ (modules test_response)
+ (forbidden_libraries base)
+ (libraries cohttp alcotest))
+
+(rule
+ (alias runtest)
+ (package cohttp)
+ (action
+  (run ./test_response.exe)))
index 1feabe0fd20a6744edd59b0fd2b8e952373c8ab4..43a4980fd85b3dae4372f65eca661d20861bd4a4 100644 (file)
@@ -70,6 +70,50 @@ let test_resolve_local_file () =
       ("cwd-docroot cwd", ".", "./buzz", "./buzz");
       ("cwd-docroot blank-path", ".", "https://example.com", "./");
       ("cwd-docroot blank-uri", ".", "", "./");
+      ( "percent-encoded slash grandparent blocked",
+        "/foo/bar/baz",
+        "..%2f..%2fbuzz",
+        "/foo/bar/baz/buzz" );
+      ( "fully percent-encoded grandparent blocked",
+        "/foo/bar/baz",
+        "%2e%2e%2f%2e%2e%2fbuzz",
+        "/foo/bar/baz/buzz" );
+      ( "double percent-encoded grandparent blocked",
+        "/foo/bar/baz",
+        "..%252f..%252fbuzz",
+        "/foo/bar/baz/..%2f..%2fbuzz" );
+      ( "double fully percent-encoded grandparent blocked",
+        "/foo/bar/baz",
+        "%252e%252e%252f%252e%252e%252fbuzz",
+        "/foo/bar/baz/%2e%2e%2f%2e%2e%2fbuzz" );
+      ( "triply percent-encoded grandparent blocked",
+        "/foo/bar/baz",
+        "..%25252f..%25252fbuzz",
+        "/foo/bar/baz/..%252f..%252fbuzz" );
+      ( "encoded space preserved",
+        "/foo/bar/baz",
+        "/my%20file.txt",
+        "/foo/bar/baz/my file.txt" );
+      ( "encoded utf8 preserved",
+        "/foo/bar/baz",
+        "/caf%C3%A9.txt",
+        "/foo/bar/baz/caf\xc3\xa9.txt" );
+      ( "encoded NUL preserved",
+        "/foo/bar/baz",
+        "/secret%00.txt",
+        "/foo/bar/baz/secret\x00.txt" );
+      ( "encoded newline preserved",
+        "/foo/bar/baz",
+        "/a%0ab.txt",
+        "/foo/bar/baz/a\nb.txt" );
+      ( "encoded DEL preserved",
+        "/foo/bar/baz",
+        "/a%7fb.txt",
+        "/foo/bar/baz/a\x7fb.txt" );
+      ( "encoded control does not collapse onto plain name",
+        "/foo/bar/baz",
+        "/sol%001.html",
+        "/foo/bar/baz/sol\x001.html" );
     ]
   in
   List.iter
@@ -79,10 +123,49 @@ let test_resolve_local_file () =
         (Cohttp.Path.resolve_local_file ~docroot ~uri:(Uri.of_string uri)))
     tests
 
+let test_normalise () =
+  let tests =
+    [
+      ("relative simple", "/foo/bar", "foo/bar");
+      ("relative dot", "/foo/./bar", "foo/bar");
+      ("relative grandparent blocked", "/../../etc/passwd", "etc/passwd");
+      ("encoded slash traversal blocked", "/..%2f..%2fetc/passwd", "etc/passwd");
+      ( "absolute-form grandparent blocked",
+        "http://host/../../etc/passwd",
+        "etc/passwd" );
+      ( "absolute-form deep grandparent blocked",
+        "https://example.com/a/../../../../etc/shadow",
+        "etc/shadow" );
+      ("root path empty", "/", "");
+      ("empty path", "", "");
+      ("trailing slash dropped", "/foo/bar/", "foo/bar");
+      ("encoded trailing slash dropped", "/foo/bar%2f", "foo/bar");
+      ("empty segments collapsed", "/foo//bar", "foo/bar");
+      ("encoded control preserved", "/sol%001.html", "sol\x001.html");
+      ("plain slash separator", "/private/secret.txt", "private/secret.txt");
+      ( "backslash preserved, distinct from slash",
+        "/private%5csecret.txt",
+        "private\\secret.txt" );
+      ( "encoded backslash traversal preserved verbatim",
+        "/..%5c..%5cwindows",
+        "..\\..\\windows" );
+    ]
+  in
+  List.iter
+    (fun (name, uri, expected) ->
+      Alcotest.(check string)
+        name expected
+        (Cohttp.Path.normalise (Uri.of_string uri)))
+    tests
+
 let () = Printexc.record_backtrace true
 
 let () =
   Alcotest.run "test_path"
     [
-      ("Path", [ ("Check resolve_local_file", `Quick, test_resolve_local_file) ]);
+      ( "Path",
+        [
+          ("Check resolve_local_file", `Quick, test_resolve_local_file);
+          ("Check normalise", `Quick, test_normalise);
+        ] );
     ]
diff --git a/cohttp/test/test_response.ml b/cohttp/test/test_response.ml
new file mode 100644 (file)
index 0000000..d15f7ab
--- /dev/null
@@ -0,0 +1,148 @@
+open Cohttp
+module String_io = Cohttp.Private.String_io
+module Response_io = Response.Private.Make (String_io.M)
+
+let t_encoding =
+  Alcotest.testable
+    (fun fmt e -> Format.pp_print_string fmt (Transfer.string_of_encoding e))
+    ( = )
+
+let check_encoding name expected response =
+  Alcotest.check t_encoding name expected (Response.encoding response)
+
+let check_no_framing_headers name response =
+  let headers = Response.headers response in
+  Alcotest.(check (option string))
+    (name ^ ": no transfer-encoding header")
+    None
+    (Header.get headers "transfer-encoding");
+  Alcotest.(check (option string))
+    (name ^ ": no content-length header")
+    None
+    (Header.get headers "content-length")
+
+let bodyless_statuses =
+  [
+    `Continue;
+    `Switching_protocols;
+    `Processing;
+    `Checkpoint;
+    `No_content;
+    `Not_modified;
+    `Code 199;
+  ]
+
+let iter_bodyless f =
+  List.iter
+    (fun status -> f (Code.string_of_status status) status)
+    bodyless_statuses
+
+let default_encoding_is_chunked () =
+  check_encoding "unspecified encoding defaults to chunked" Transfer.Chunked
+    (Response.make ())
+
+let explicit_encoding_is_used () =
+  check_encoding "explicit encoding is used" (Transfer.Fixed 5L)
+    (Response.make ~status:`OK ~encoding:(Transfer.Fixed 5L) ())
+
+let encoding_from_headers () =
+  check_encoding "encoding taken from supplied headers" (Transfer.Fixed 42L)
+    (Response.make ~status:`OK
+       ~headers:(Header.of_list [ ("content-length", "42") ])
+       ())
+
+let bodyless_has_no_encoding () =
+  iter_bodyless (fun name status ->
+      let r = Response.make ~status () in
+      check_encoding (name ^ " is not chunked by default") Transfer.Unknown r;
+      check_no_framing_headers name r)
+
+let bodyless_ignores_explicit_encoding () =
+  iter_bodyless (fun name status ->
+      let chunked = Response.make ~status ~encoding:Transfer.Chunked () in
+      check_encoding
+        (name ^ " drops an explicit chunked encoding")
+        Transfer.Unknown chunked;
+      check_no_framing_headers (name ^ " (chunked)") chunked;
+      let fixed = Response.make ~status ~encoding:(Transfer.Fixed 0L) () in
+      check_encoding
+        (name ^ " drops an explicit fixed encoding")
+        Transfer.Unknown fixed;
+      check_no_framing_headers (name ^ " (fixed)") fixed)
+
+let t_has_body =
+  Alcotest.testable
+    (fun fmt -> function
+      | `No -> Format.pp_print_string fmt "`No"
+      | `Unknown -> Format.pp_print_string fmt "`Unknown"
+      | `Yes -> Format.pp_print_string fmt "`Yes")
+    ( = )
+
+let bodyless_has_no_body () =
+  iter_bodyless (fun name status ->
+      Alcotest.check t_has_body (name ^ " has no body") `No
+        (Response.has_body (Response.make ~status ())))
+
+let write_response r =
+  let buf = Buffer.create 128 in
+  Response_io.write ~flush:false (fun _ -> ()) r buf;
+  Buffer.contents buf
+
+let write_chunked_response () =
+  Alcotest.(check string)
+    "a 200 response is framed as chunked"
+    "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n0\r\n\r\n"
+    (write_response (Response.make ()))
+
+let write_switching_protocols () =
+  let r =
+    Response.make ~status:`Switching_protocols
+      ~headers:
+        (Header.of_list [ ("upgrade", "websocket"); ("connection", "Upgrade") ])
+      ()
+  in
+  Alcotest.(check string)
+    "a 101 response is written without any framing"
+    "HTTP/1.1 101 Switching Protocols\r\n\
+     upgrade: websocket\r\n\
+     connection: Upgrade\r\n\
+     \r\n"
+    (write_response r)
+
+let write_no_content () =
+  Alcotest.(check string)
+    "a 204 response is written without any framing"
+    "HTTP/1.1 204 No Content\r\n\r\n"
+    (write_response (Response.make ~status:`No_content ()))
+
+let write_not_modified () =
+  Alcotest.(check string)
+    "a 304 response is written without any framing"
+    "HTTP/1.1 304 Not Modified\r\n\r\n"
+    (write_response (Response.make ~status:`Not_modified ()))
+
+let () =
+  Alcotest.run "test_response"
+    [
+      ( "Encoding",
+        [
+          ("default is chunked", `Quick, default_encoding_is_chunked);
+          ("explicit encoding", `Quick, explicit_encoding_is_used);
+          ("from headers", `Quick, encoding_from_headers);
+        ] );
+      ( "Bodyless statuses",
+        [
+          ("no encoding by default", `Quick, bodyless_has_no_encoding);
+          ( "explicit encoding ignored",
+            `Quick,
+            bodyless_ignores_explicit_encoding );
+          ("no body", `Quick, bodyless_has_no_body);
+        ] );
+      ( "Serialization",
+        [
+          ("chunked", `Quick, write_chunked_response);
+          ("101 Switching Protocols", `Quick, write_switching_protocols);
+          ("204 No Content", `Quick, write_no_content);
+          ("304 Not Modified", `Quick, write_not_modified);
+        ] );
+    ]
index 2d5e1d3f16cac4da62a9ecf962557548cd958812..c30d0087d515b89f3a3734d2523c63ab7ae64045 100644 (file)
@@ -1,7 +1,7 @@
 (lang dune 3.8)
 
 (name cohttp)
-(version v6.2.2)
+(version v6.3.0)
 
 (license ISC)
 
   "An implementation of an HTTP client and server using the Async\nconcurrency library. See the `Cohttp_async` module for information\non how to use this.  The package also installs `cohttp-curl-async`\nand a `cohttp-server-async` binaries for quick uses of a HTTP(S)\nclient and server respectively.\n")
  (depends
   (ocaml
-   (and (>= 4.14) (< 5.3.0)))
+   (>= 4.14))
   (http
    (= :version))
   (cohttp
   (conduit-mirage
    (>= 8.0.0))
   (mirage-kv
-   (>= 3.0.0))
+   (>= 6.0.1))
   (lwt
    (>= 2.4.3))
   (cohttp-lwt
    (>= 6.0.0))
   (fmt
    (>= 0.8.7))
-  astring
   magic-mime
   (ppx_sexp_conv
    (>= v0.13.0))
+  (alcotest
+   (and
+    :with-test
+    (>= 1.7.0)))
   (cohttp
    (= :version))))
 
  (depends
   (ocaml
    (>= 4.08))
-  (ocurl (>= 0.9.2))
+  (curl (>= 0.10.0))
   (http
    (= :version))
   stringext))
  (depends
   (ocaml
    (>= 4.08))
-  (ocurl (>= 0.9.2))
+  (curl (>= 0.10.0))
   (http
    (= :version))
   (cohttp-curl
  (description
   "An HTTP client that relies on Curl + Async for the backend. Does not require\nconduit for SSL.")
  (depends
-  (ocurl (>= 0.9.2))
+  (curl (>= 0.10.0))
   (http
    (= :version))
   stringext
index 0dd932718a995a91fd512cf2dfd9e3c555d0b913..9b53d31da2248f3dfba5c3b13b294a97e3cd6694 100644 (file)
--- a/flake.nix
+++ b/flake.nix
           };
           cohttp-mirage = pkg {
             pname = "cohttp-mirage";
+            checkInputs = [ alcotest ];
             propagatedBuildInputs = [
               mirage-flow mirage-channel conduit conduit-mirage
-              mirage-kv lwt cohttp-lwt cstruct fmt astring magic-mime ppx_sexp_conv
+              mirage-kv lwt cohttp-lwt cstruct fmt magic-mime ppx_sexp_conv
             ];
           };
           cohttp-bench = pkg {
index 7b13443ce33bf3df1f97017a8e4b8ee8ad2a04ac..284106d251866af7d24e2cbfb366a073d9008253 100644 (file)
--- a/http.opam
+++ b/http.opam
@@ -1,4 +1,4 @@
-version: "6.2.2"
+version: "6.3.0"
 # This file is generated by dune, edit dune-project instead
 opam-version: "2.0"
 synopsis: "Type definitions of HTTP essentials"
index b90e11b2429a7b0b6a48f43cf5fd3f37f560f2f4..5f8ff2d94200e66297f5e3fcfc7fa75b274fef88 100644 (file)
@@ -204,10 +204,10 @@ module Header = struct
     let b = Buffer.create 128 in
     to_list h
     |> List.iter (fun (k, v) ->
-           Buffer.add_string b k;
-           Buffer.add_string b ": ";
-           Buffer.add_string b v;
-           Buffer.add_string b "\r\n");
+        Buffer.add_string b k;
+        Buffer.add_string b ": ";
+        Buffer.add_string b v;
+        Buffer.add_string b "\r\n");
     Buffer.add_string b "\r\n";
     Buffer.contents b
 
@@ -569,6 +569,10 @@ module Status = struct
     | `Network_connect_timeout_error -> 599
     | `Code code -> code
 
+  (* Defined for status codes in RFC7230 *)
+  let body_allowed t =
+    match to_int t with 204 | 304 -> false | code -> code < 100 || code >= 200
+
   let reason_phrase_of_code : int -> string = function
     | 100 -> "Continue"
     | 101 -> "Switching Protocols"
index 32713cf64c66ba7f4752e9e3cd2ff4af9157116d..4a3351ca58e5b941575234ca1cdfb03bfa238200 100644 (file)
@@ -153,6 +153,15 @@ module Status : sig
   val to_string : t -> string
   val to_int : t -> int
   val of_int : int -> t
+
+  val body_allowed : t -> bool
+  (** [body_allowed status] returns whether a response with [status] allows a
+      payload body to be present per RFC7230. A [1xx], [204] or [304] response
+      is always terminated by the first empty line after the header fields, and
+      so carries neither a body nor any framing header.
+
+      https://www.rfc-editor.org/rfc/rfc7230#section-3.3 *)
+
   val reason_phrase_of_code : int -> string
   val pp : Format.formatter -> t -> unit
 end
@@ -341,19 +350,19 @@ module Header : sig
       example below) are not affected by [clean_dup]. For example,
 
       {v
-    transfer-encoding: gzip
-    host: afirsthost.com
-    connection: keep-alive
-    host: anhost.com, anotherhost.com
-    transfer-encoding: chunked
+      transfer-encoding: gzip
+      host: afirsthost.com
+      connection: keep-alive
+      host: anhost.com, anotherhost.com
+      transfer-encoding: chunked
       v}
 
       becomes
 
       {v
-    transfer-encoding: gzip, chunked
-    connection: keep-alive
-    host: anhost.com, anotherhost.com
+      transfer-encoding: gzip, chunked
+      connection: keep-alive
+      host: anhost.com, anotherhost.com
       v}
 
       Finally, following
index e3803941c6861a8608a0c2f8cdb62c1ff4b30a8e..e85b125a3a8a688fa902f5d6d15b0618df491884 100644 (file)
@@ -6,7 +6,7 @@ let requires_content_length_tests =
   let valid_meth () =
     [ `POST; `PUT; `PATCH ]
     |> List.map (fun meth ->
-           Request.make ~meth "p" |> Request.requires_content_length)
+        Request.make ~meth "p" |> Request.requires_content_length)
     |> List.for_all Fun.id
     |> aeb "requires_content_length m = true, where m is `POST, `PUT or `PATCH"
          true
@@ -14,7 +14,7 @@ let requires_content_length_tests =
   let invalid_meth () =
     [ `GET; `HEAD; `DELETE; `OPTIONS; `TRACE; `CONNECT; `Other "h" ]
     |> List.map (fun meth ->
-           Request.make ~meth "p" |> Request.requires_content_length)
+        Request.make ~meth "p" |> Request.requires_content_length)
     |> List.for_all not
     |> aeb
          {| requires_content_length m = false, where m is `GET; `HEAD;`DELETE;`OPTIONS;`TRACE; `CONNECT;`Other "h" |}
@@ -29,14 +29,14 @@ let content_length_tests =
   let some_x () =
     [ (`POST, "0"); (`PUT, "233"); (`PATCH, "012345") ]
     |> List.map (fun (meth, len) ->
-           match
-             Request.make ~meth
-               ~headers:(Header.of_list [ ("Content-Length", len) ])
-               "p"
-             |> Request.content_length
-           with
-           | Some x -> int_of_string len = x
-           | None -> false)
+        match
+          Request.make ~meth
+            ~headers:(Header.of_list [ ("Content-Length", len) ])
+            "p"
+          |> Request.content_length
+        with
+        | Some x -> int_of_string len = x
+        | None -> false)
     |> List.for_all Fun.id
     |> aeb "content_length t = Some x" true
   in
@@ -44,14 +44,14 @@ let content_length_tests =
   let none () =
     [ (`POST, "-1"); (`PUT, "-233"); (`PATCH, "abc") ]
     |> List.map (fun (meth, len) ->
-           match
-             Request.make ~meth
-               ~headers:(Header.of_list [ ("Content-Length", len) ])
-               "p"
-             |> Request.content_length
-           with
-           | Some _ -> false
-           | None -> true)
+        match
+          Request.make ~meth
+            ~headers:(Header.of_list [ ("Content-Length", len) ])
+            "p"
+          |> Request.content_length
+        with
+        | Some _ -> false
+        | None -> true)
     |> List.for_all Fun.id
     |> aeb "content_length t = None" true
   in
@@ -59,9 +59,9 @@ let content_length_tests =
   let method_ () =
     [ `GET; `HEAD; `DELETE; `OPTIONS; `TRACE; `CONNECT; `Other "h" ]
     |> List.map (fun meth ->
-           match Request.make ~meth "p" |> Request.content_length with
-           | Some _ -> false
-           | None -> true)
+        match Request.make ~meth "p" |> Request.content_length with
+        | Some _ -> false
+        | None -> true)
     |> List.for_all Fun.id
     |> aeb "content_length t = None" true
   in
index 26154fe3b0f87becf78e030c06f6a121ced58f38..c0e9b2969db89b28ede70de5b1a92fad564b2736 100644 (file)
@@ -56,6 +56,32 @@ let content_length_tests =
   ( "content_length",
     [ ("OK", `Quick, ok_status); ("`No_content", `Quick, no_content_status) ] )
 
+let body_allowed_tests =
+  let case (status, expected) =
+    ( Status.to_string status,
+      `Quick,
+      fun () ->
+        Status.body_allowed status
+        |> aeb ("body_allowed " ^ Status.to_string status) expected )
+  in
+  ( "body_allowed",
+    List.map case
+      [
+        (`Continue, false);
+        (`Switching_protocols, false);
+        (`Processing, false);
+        (`Checkpoint, false);
+        (`No_content, false);
+        (`Not_modified, false);
+        (`Code 199, false);
+        (`OK, true);
+        (`Created, true);
+        (`Found, true);
+        (`Bad_request, true);
+        (`Internal_server_error, true);
+        (`Code 250, true);
+      ] )
+
 let () =
   Alcotest.run "test_response"
-    [ requires_content_length_tests; content_length_tests ]
+    [ requires_content_length_tests; content_length_tests; body_allowed_tests ]